Interactive Transaction
一部が失敗すると全体がrollbackされる
throwされるので、try..catch内に書くなどする
例
code:ts
const result = await prisma.$transaction(async (tx) => {
const from = await tx.account.update({
where: { id: 1 },
data: { balance: { decrement: 100 } },
});
if (from.balance < 0) {
throw new Error("残高不足"); // ← throw すると全部ロールバック
}
await tx.account.update({
where: { id: 2 },
data: { balance: { increment: 100 } },
});
});
オプション
code:ts
await prisma.$transaction(
async (tx) => { /* ... */ },
{
maxWait: 2000, // トランザクション開始のためのコネクション取得待ち上限(ms)
timeout: 5000, // コールバック全体の実行制限時間(ms)。超えると中断&rollback
isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
}
);
table:_
オプション 意味 デフォルト
maxWait トランザクション枠(コネクション)を待つ最大時間 2000ms
timeout コールバック実行に許される最大時間 5000ms
isolationLevel 分離レベル(DBごとに対応値が異なる) DB依存
注意点・ベストプラクティス
1. コールバックは短く保つ
トランザクション中は DB コネクションを掴みっぱなしになります。中で外部 API 呼び出しや重い処理(メール送信、HTTP リクエスト)を待つと、timeout に引っかかったりコネクションプールが枯渇します。外部 I/O はトランザクションの外で。
code:ts
// ❌ ダメな例
await prisma.$transaction(async (tx) => {
const order = await tx.order.create(...);
await sendEmail(order); // 外部I/O — コネクションを無駄に占有
});
// ✅ 外に出す
const order = await prisma.$transaction(async (tx) => tx.order.create(...));
await sendEmail(order);
2. 戻り値はコールバックの return がそのまま返る
const x = await prisma.$transaction(async (tx) => { return ... }) で受け取れます。
3. リトライは自前で
Serializable などでは write conflict / deadlock(Prisma エラーコード P2034)が起きえます。Prisma は自動リトライしないので、必要なら自分でリトライループを書きます。
code:ts
async function withRetry<T>(fn: () => Promise<T>, retries = 3): Promise<T> {
for (let i = 0; ; i++) {
try {
return await fn();
} catch (e) {
if (e.code === "P2034" && i < retries) continue; // 競合なら再試行
throw e;
}
}
}
4. ネストは不可
tx の中でさらに tx.$transaction(...) はできません。1つのコールバック内で完結させます。
5. 長時間トランザクションはアンチパターン
timeout を闇雲に伸ばすより、トランザクションのスコープ自体を小さくする設計を優先します。